package main;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.rmi.AccessException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.UnmarshalException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListResourceBundle;

import org.apache.commons.lang3.SystemUtils;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.PatternLayout;
import org.apache.zookeeper.AsyncCallback.StatCallback;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

import window.Console;
import comunicacao.GestorZooKeeper;
import comunicacao.MessageRedirectorInterface;
import comunicacao.ReceiveMessageInterface;
import comunicacao.primaryDBInterface;
import entities.CachedTeatro;
import entities.ConfigInfo;
import entities.Range;
import entities.ServerInfo;
import entities.Teatro;

/**
 * Controlador principal da base de dados. Gere a conexao com o ZooKeeper
 * e e responsavel pelas funcoes de uma base de dados primaria
 * 
 * @author Leonardo Lino Nº45340
 * @author Diogo Pires Nº40351
 */
public class DBmaster extends java.rmi.server.UnicastRemoteObject implements
Watcher, MessageRedirectorInterface, primaryDBInterface {

	public static final int SHARDING_LEVEL_0 = 0;
	public static final int SHARDING_LEVEL_1 = 1;
	public static final int SHARDING_LEVEL_2 = 2;

	public static final int REMOVAL_LEVEL_1 = 1;
	public static final int REMOVAL_LEVEL_2 = 2;
	public static final int REMOVAL_LEVEL_3 = 3;

	public final boolean USE_FILE_ADDRESS = true;
	private boolean disabled;

	private Registry registry;
	private GestorZooKeeper zKeeper;
	private String zKeeperAddressPort;
	private MembershipManager mManager;
	private Watcher eWatcher;

	private final String CURRENT_DIR;
	private final String RUNNING_LOCATION;
	private final String OS;
	private final String CONFIG_FILE = "config.txt";
	private final String SNAPSHOT_FILE = "snapshot.txt";
	private final String SNAPSHOT_MANAGER_FILE = "snapshotManager.txt";
	private final int SESSION_TIMEOUT = 5000;
	private String eol = System.getProperty("line.separator");
	private final String masterSeparator = "#dbm#";

	private boolean fileOverride;
	private boolean startFromState;
	private String thisAddress;
	private String thisfileAddress;
	private String baseLocation;
	private int thisPort;
	private int rows;
	private int columns;
	private int numtheaters;
	private int cacheLimit;
	int listTroundRobin;

	private int server_id;
	private String dbRegistryName;
	private boolean isLeader;
	private int leaderPort;
	private String leaderAddress;
	private String leaderBindName;
	private int leaderID;

	private Object signal;
	private ArrayList<String> nodeData;
	private ArrayList<ServerInfo> servers;
	private ArrayList<ServerInfo> reentrantServers;
	private ArrayList<ServerInfo> serversInMaintenance;
	private LinkedHashMap<String, Integer> theatersMap;
	Thread DBHandler;

	/**
	 * Creates a new database master object that will be responsible
	 * for primary functions and for the ZooKeeper connection and events
	 * 
	 * @throws RemoteException
	 */
	public DBmaster() throws RemoteException{
		disabled = false;
		startFromState = false;
		String managerData = null;

		String everything = "";
		OS = SystemUtils.OS_NAME;
		ConfigInfo config = new ConfigInfo();

		CURRENT_DIR = System.getProperty("user.dir") + File.separator;

		if(SystemUtils.IS_OS_LINUX){
			String temp = FileOperator.path(new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()), true,true);
			RUNNING_LOCATION = temp.replace("%20"," ");
		}else if(SystemUtils.IS_OS_WINDOWS)
			RUNNING_LOCATION = CURRENT_DIR;
		else
			RUNNING_LOCATION = FileOperator.path(new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()), true,true);


		System.out.println("Operating System: " + OS);
		System.out.println("Execution File Directory: "+RUNNING_LOCATION);
		System.out.println("Current Directory: "+CURRENT_DIR);


		// Open config file
		try {
			File file = new File(RUNNING_LOCATION + CONFIG_FILE);

			FileReader fr = new FileReader(file);
			BufferedReader br = new BufferedReader(fr);

			StringBuilder sb = new StringBuilder();
			String line = br.readLine();

			while (line != null) {
				sb.append(line);
				sb.append(eol);
				line = br.readLine();
			}

			br.close();
			everything = sb.toString();
		} catch (IOException e) {
		}

		String args[] = everything.split("\\s+");

		// Load config file
		for (int i = 0; i < args.length; i++) {
			String[] lineChunks = args[i].split(":");
			if (lineChunks[0].equals("Port")){
				thisPort = Integer.parseInt(lineChunks[1]);
				config.setThisPort(thisPort);
			}else if (lineChunks[0].equals("Storage")) {
				if (lineChunks.length != 1) {
					baseLocation = lineChunks[1];
					config.setBaseLocation(baseLocation);
				} else{
					baseLocation = "";
					config.setBaseLocation(baseLocation);
				}
			} else if (lineChunks[0].equals("NumberTheaters")){
				numtheaters = Integer.parseInt(lineChunks[1]);
				config.setNumtheaters(numtheaters);
			}else if (lineChunks[0].equals("Rows")){
				rows = Integer.parseInt(lineChunks[1]);
				config.setRows(rows);
			}else if (lineChunks[0].equals("Columns")){
				columns = Integer.parseInt(lineChunks[1]);
				config.setColumns(columns);
			}else if (lineChunks[0].equals("CacheTime")){
				cacheLimit = Integer.parseInt(lineChunks[1]);
				config.setCacheLimit(cacheLimit);
			}else if (lineChunks[0].equals("Override")){
				fileOverride = Boolean.parseBoolean(lineChunks[1]);
				config.setFileOverride(fileOverride);
			}else if (lineChunks[0].equals("ZooKeeper")){
				zKeeperAddressPort = lineChunks[1] + ":" + lineChunks[2];
			}else if (lineChunks[0].equals("Address")){
				thisfileAddress = lineChunks[1];
			}
		}

		//use the address provided by the configuration file
		if(USE_FILE_ADDRESS){
			thisAddress = thisfileAddress;
		}else{
			try {
				// get the address of this host.
				thisAddress = (InetAddress.getLocalHost().getHostAddress());
			} catch (UnknownHostException e) {
				System.out.println("Error: could not retrieve Host Address");
			}
		}

		System.out.println("this address=" + thisAddress + ", port=" + thisPort);
		System.out.println("----------------------------------------------------" + eol);

		config.setThisAddress(thisAddress);

		//generate the theathers <name,id> hash
		genTheatersHash();

		//Inicialize the listStructures
		servers = new ArrayList<ServerInfo>();
		reentrantServers = new ArrayList<ServerInfo>();
		serversInMaintenance = new ArrayList<ServerInfo>();
		nodeData = new ArrayList<String>();

		//define the watcher object for ZooKeeper events
		eWatcher = this;

		///////////////////////////////////////////////////////////
		//RETRIEVE PREVIOUS STATE FROM DISK
		///////////////////////////////////////////////////////////
		if(!fileOverride){

			try{
				loadSystemSnapshot(getSystemSnapshot(false));
			}catch(Exception e){
				e.printStackTrace();
			}

			if(isLeader){
				startFromState = true;
				System.out.println(eol + "Starting primary from previous state");
				
				for(ServerInfo serv: servers){
					serv.setAvailable(false);
					serv.setInMaintenance(true);
				}

				if(servers.isEmpty())
					startFromState = false;
			}else{
				System.out.println(eol + "Starting server from previous state");

				try{
					managerData = getSystemSnapshot(true);
				}catch(Exception e){
					e.printStackTrace();
				}
			}
		}
		///////////////////////////////////////////////////////////

		//isto agora ta no startZookeeper a seguir a cena dos id's
		//nodeData.add(thisAddress+":"+Integer.toString(thisPort)+":dbServer"+Integer.toString(server_id));
		
		if(!fileOverride){
			nodeData.add(Integer.toString(server_id));
			nodeData.add("ready");
		}

		//Inicialize the ZooKeeper client and connect to the database system
		startZooKeeperClient(zKeeperAddressPort, fileOverride);

		if(fileOverride)
			nodeData.add(Integer.toString(server_id));

		config.setId(server_id);
		dbRegistryName = "dbServer" + Integer.toString(server_id);

		///////////////////////////////////////////////////////////
		//If leader, creates the primary RMI registry.
		//If not, starts a database manager
		///////////////////////////////////////////////////////////
		if(!isLeader){
			signal = new Object();
			ServerInfo leaderInfo = new ServerInfo(leaderAddress, leaderPort, leaderID);
			leaderInfo.setBindName(leaderBindName);
			startDatabase(DBHandler, config, signal, zKeeper, leaderInfo, managerData);

			if(fileOverride){
				synchronized (signal) {
					try {
						signal.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}

				nodeData.add("ready");
				zKeeper.setData("/DBServers/DB-"+Integer.toString(server_id), nodeDataToString(nodeData), -1);
			}
		}else{
			try {
				// create the registry and bind the name and object.
				registry = LocateRegistry.createRegistry(thisPort);
				registry.rebind(dbRegistryName, this);
			} catch (RemoteException e) {
				System.out.println("ERROR: could not create master registry");
				e.printStackTrace();
				throw e;
			}
		}

		//If starting a new state, store an inicial snapshot
		if(fileOverride)
			StoreSystemSnapshot();
	}

	/**
	 * Starts a new database manager thread that will be responsible for inicializing
	 * and running the database manager with the informations suplied by the master
	 * 
	 * @param thread		- The thread used to run the manager
	 * @param config		- The configuration structure with the necessary parameters
	 * @param signal		- An object provided to the manager to signal its readiness to the master
	 * @param zKeeper		- The ZooKeeper manager used by this server
	 * @param leaderInfo	- A server object containing the leader's information
	 * @param managerData	- A string containing the managers previous state (used if inicializing from a disk stored state) 
	 */
	private void startDatabase(Thread thread,ConfigInfo config, Object signal, GestorZooKeeper zKeeper, ServerInfo leaderInfo, String managerData){
		Runnable task = new DBThread(config, signal, zKeeper, leaderInfo, managerData);
		thread = new Thread(task);
		thread.setName("DBHandler");
		thread.start();
	}

	/**
	 * Connects to the suplied ZooKeeper address and builds the internal znode
	 * structure required by the database funcionality. If this isnt the leader,
	 * it retrieves the necessary information stored in ZooKeeper.
	 * 
	 * @param addressPort	-The ZooKeeper's address and port	
	 * @param newStart		-Indicates if the system is performing a fresh state start or a previous state start
	 * 
	 * @return	-The sucess/failure result of all steps executed
	 */
	private boolean startZooKeeperClient(String addressPort, boolean newStart){
		System.out.println("------------STARTING ZOOKEEPER CLIENT------------");

		boolean result = true;
		zKeeper = new GestorZooKeeper();
		mManager = new MembershipManager(zKeeper, eWatcher);

		try {
			ConsoleAppender console = new ConsoleAppender(); //create appender
			//configure the appender
			String PATTERN = "%d [%p|%c|%C{1}] %m%n";
			console.setLayout(new PatternLayout(PATTERN)); 
			console.setThreshold(Level.ERROR);
			console.activateOptions();

			//sets the log4j with the provided configuration
			org.apache.log4j.BasicConfigurator.configure(console);
			//establishes the connection with the ZooKeeper server
			zKeeper.connect(addressPort, SESSION_TIMEOUT);
		} catch (IOException e) {
			System.out.println("Error while connecting to ZooKeeper service");
			e.printStackTrace();
			result = false;
		} catch (InterruptedException e) {
			System.out.println("Error while connecting to ZooKeeper service");
			e.printStackTrace();
			result = false;
		}

		if (result) {

			if(!mManager.checkGlobalGroup(null))
				mManager.createGlobalGroup();

			if(!mManager.checkGlobalConfig(null)){
				String gConfigData = Integer.toString(numtheaters) +","+rows+","+columns; 
				mManager.createGlobalConfig(gConfigData);
			}else{
				String gConfigData = Integer.toString(numtheaters) +","+rows+","+columns;
				mManager.setGlobalConfigData(gConfigData);
			}

			if(!mManager.checkDBSystemState(null))
				mManager.createDBSystemState();

			// Generates the database znode group if not already created
			if (!mManager.checkDBGroup(null))
				mManager.createDBGroup();

			// Cria o grupo Ready
			if (!mManager.checkReadyGroup(null))
				mManager.createReadyGroup();

			if(newStart)
				if(!mManager.checkIDTracker(null)){
					if(mManager.createIDTracker()){
						server_id = 1;
						mManager.setCurrentID(Integer.toString(server_id));
					}else{
						result = false;
					}
				}else{
					String current = mManager.getCurrentID(null);

					if(current != null){
						server_id = Integer.parseInt(current) + 1;
						mManager.setCurrentID(Integer.toString(server_id));
					}else{
						result = false;
						System.out.println("Error: Could not retrieve current ID");
					}
				}
			
			nodeData.add(thisAddress+":"+Integer.toString(thisPort)+":dbServer"+Integer.toString(server_id));

			// Adiciona-se ao grupo de servidores
			if (!mManager.JoinDBGroup(nodeDataToString(nodeData), server_id)){
				result = false;
				isLeader = false;
				System.out.println("Failed joining process");
			}else {
				// Cria o sinalizador Ready das Bases de Dados
				// Adiciona uma watch
				if (!mManager.checkDBReady(null)) {
					if (mManager.createDBReady(null)){
						mManager.setDBReadyData(thisAddress,
								Integer.toString(thisPort),
								Integer.toString(server_id));
						isLeader = true;
					}else
						result = false;
				} else {
					isLeader = false;

					//Retrives informations about the leader and sets the
					//watch that alerts the servers to the failure of the primary
					String data = mManager.getDBReadyData(this);
					if (data != null) {
						try {
							String[] dataParts = data.split(",");
							leaderAddress = dataParts[0].split(":")[0];
							leaderPort = Integer.parseInt(dataParts[0]
									.split(":")[1]);
							leaderBindName = dataParts[0].split(":")[2];
							leaderID = Integer.parseInt(dataParts[1]);
						} catch (Exception e) {
							System.out.println("Error reading data");
							e.printStackTrace();
						}
					} else {
						System.out.println("Ready DB data was null");
						result = false;
					}
				}
			}
		}

		if(isLeader){
			zKeeper.setGroupWatcher("/DBServers", eWatcher);
		}

		System.out.println();
		System.out.println("Server ID: " + server_id);
		System.out.println("Leader status: " + isLeader);
		System.out.println("ZooKeeper initial connection setup: " + result);

		System.out.println("----------------------------------------------------------------------");
		return result;
	}

	private void genTheatersHash() {
		theatersMap = new LinkedHashMap<String, Integer>(500);
		String name;

		for (int i = 1; i <= numtheaters; i++) {
			name = "teatro" + Integer.toString(i);
			theatersMap.put(name, i);
		}
	}

	//Selects a database server to provide data to a new database server
	public ServerInfo selectProvider() {
		ServerInfo server = null;
		int largestRange = 0;

		ArrayList<ServerInfo> availableServers = new ArrayList<ServerInfo>();

		for(ServerInfo serv: servers){
			if(serv.isAvailable()){
				availableServers.add(serv);
			}
		}

		if(availableServers.size() != 0){
			for(ServerInfo serv: availableServers){
				if(serv.getMainRange().getSize() > largestRange){
					server = serv;
					largestRange = serv.getMainRange().getSize();
				}
			}
		}

		return server;
	}

	public ServerInfo getProviderReplic(int providerID){
		ServerInfo server = null;

		for(ServerInfo serv: servers){
			if(serv.isAvailable()){
				if(serv.getCoordinatorRange().getResponsable() == providerID){
					server = serv;
					break;
				}
			}
		}

		return server;
	}

	public ServerInfo getProviderCoordinator(int providerID){
		ServerInfo server = null;

		for(ServerInfo serv: servers){
			if(serv.isAvailable()){
				if(serv.getReplicRange().getResponsable() == providerID){
					server = serv;
					break;
				}
			}
		}

		return server;
	}

	public String nodeDataToString(ArrayList<String> node){
		StringBuilder sb = new StringBuilder();

		for(int i=0; i<node.size(); i++){

			if(i != node.size() -1){
				sb.append(node.get(i));
				sb.append(',');
			}else
				sb.append(node.get(i));
		}

		return sb.toString();
	}

	@Override
	public void process(WatchedEvent event) {
		switch (event.getType()) {
		case NodeChildrenChanged:
			System.out.println(eol + "-----------------------EVENT------------------------");
			System.out.println("Reveived children change event");
			System.out.println("Location: " + event.getPath());

			System.out.println("Children list:");
			List<String> childrens = zKeeper.listGroup(event.getPath(), this);

			if(childrens != null)
				for (String child : childrens) {
					System.out.println("    -" + child);
				}

			for(String str: childrens){
				boolean found = false;
				boolean reentrantFound = false;
				ServerInfo reentrantServer = null;

				//Se nao for o id node ou o primary
				if(!str.equals("ID") && !str.equals("DB-"+Integer.toString(server_id)) ){
					String idStr = str.split("-")[1];
					int id = Integer.parseInt(idStr);

					//verifica se ja e conhecido
					for (ServerInfo serv : servers) {
						if (serv.getId() == id) {
							found = true;
							reentrantServer = serv;
							break;
						}
					}

					//Se nao for conhecido
					if(!found){
						String data = zKeeper.getData("/DBServers/DB-" + idStr, -1, null);
						String[] dataParts = data.split(",");
						String address = dataParts[0].split(":")[0];
						int port = Integer.parseInt(dataParts[0].split(":")[1]);

						ServerInfo newServer = new ServerInfo(address, port, id);
						newServer.setAvailable(false);
						newServer.setBindName("dbServer"+ idStr);

						zKeeper.existsZNode("/DBServers/DB-" + idStr, this);
						servers.add(newServer);

						System.out.println(eol + "New database server found!");
						System.out.println("server " + "DB-" + idStr + " added to list");
					}

					if(startFromState){

						//verifica se ja e conhecido
						for (ServerInfo serv : reentrantServers) {
							if (serv.getId() == id) {
								reentrantFound = true;
								break;
							}
						}

						if(!reentrantFound && found){
							zKeeper.existsZNode("/DBServers/DB-" + idStr, this);
							reentrantServers.add(reentrantServer);
							
							System.out.println(eol + "Server " + "DB-"+idStr + " has been restored!");
						}

						if(reentrantServers.size() == servers.size()){
							reentrantServers.clear();
							startFromState = false;
							
							for(ServerInfo serv: servers){
								serv.setAvailable(true);
								serv.setInMaintenance(false);
							}
							
							System.out.println("All servers have been restored!");
						}
					}
				}
			}

			System.out.println("--------------------------------------------------------");

			break;

		case NodeCreated:
			System.out.println(eol + "-----------------------EVENT------------------------");
			System.out.println("Received node creation event");
			System.out.println("Location: " + event.getPath());

			if(nodeName(event.getPath()).equals("DBready")){
				System.out.println(eol + "leader is ready" + eol + "Retrieving leader info...");

				if (mManager.checkDBReady(this)) {
					//Retrives informations about the leader and sets the
					//watch that alerts the servers to the failure of the primary
					String data = mManager.getDBReadyData(this);
					if (data != null) {
						try {
							String[] dataParts = data.split(",");
							leaderAddress = dataParts[0].split(":")[0];
							leaderPort = Integer.parseInt(dataParts[0]
									.split(":")[1]);
							leaderBindName = dataParts[0].split(":")[2];
							leaderID = Integer.parseInt(dataParts[1]);
						} catch (Exception e) {
							System.out.println("Error reading data");
							e.printStackTrace();
						}
					} else {
						System.out.println("Ready DB data was null");
					}

					StoreSystemSnapshot();
				}

			}

			System.out.println("--------------------------------------------------------");
			break;

		case NodeDataChanged:
			System.out.println(eol + "-----------------------EVENT------------------------");
			System.out.println("Received node data change event");
			System.out.println("Location: " + event.getPath() + eol);
			String data = zKeeper.getData(event.getPath(), -1, this); // activates a watch (DataChange/Deletion)
			String[] dataParts = data.split(",");

			if(dataParts.length > 2){
				if(dataParts[2].equals("ready")){

					//Vai encontrar o servidor correspondente ao evento (guardado)
					for(int i = 0; i< servers.size(); i++){
						if(servers.get(i).getId() == Integer.parseInt(dataParts[1])){

							System.out.println("Server DB-" + servers.get(i).getId() + " is ready for inicialization");

							//Se for o primeiro servidor
							if(servers.size() == 1){
								System.out.println("sending level 0 start conditions (FIRST SERVER)");
								Range range = new Range(1, numtheaters, servers.get(i).getId(), numtheaters);

								servers.get(i).setMainRange(range);


								servers.get(i).setAvailable(false);
								servers.get(i).setInMaintenance(true);

								serversInMaintenance.add(servers.get(i));

								MasterUtils.sendStartConditions(servers.get(i), null, null, null, range, null, null, SHARDING_LEVEL_0);

								//servers.get(i).setAvailable(true);

							}//Se nao for o primeiro servidor
							else{
								if(servers.size() == 2){
									System.out.println("sending level 1 start conditions (SECOND SERVER)");

									ServerInfo provider = selectProvider();
									System.out.println(eol + "selected provider is: DB-" + provider.getId());

									Range range = MasterUtils.calculateMainRange(provider.getMainRange(), servers.get(i).getId(), numtheaters);
									Range coordinatorRange = MasterUtils.calculateReplicRange(provider.getMainRange(), provider.getId(), numtheaters);
									Range replicRange = MasterUtils.calculateMainRange(provider.getMainRange(), provider.getId(), numtheaters);
									Range providerReplicRange = MasterUtils.calculateReplicRange(provider.getMainRange(), servers.get(i).getId(), numtheaters);

									servers.get(i).setMainRange(range); //Data hold by this server
									servers.get(i).setReplicRange(replicRange); //Data replicated from this server
									servers.get(i).setCoordinatorRange(coordinatorRange); //Data this server replicates
									servers.get(i).setIdReplic(replicRange.getResponsable());// replica id
									servers.get(i).setIdCoordinator(coordinatorRange.getResponsable()); //coordinator id

									provider.setMainRange(coordinatorRange);
									provider.setReplicRange(providerReplicRange);
									provider.setCoordinatorRange(range);
									provider.setIdReplic(providerReplicRange.getResponsable());
									provider.setIdCoordinator(range.getResponsable());


									servers.get(i).setAvailable(false);
									servers.get(i).setInMaintenance(true);

									provider.setAvailable(false);
									provider.setInMaintenance(true);

									serversInMaintenance.add(servers.get(i));
									serversInMaintenance.add(provider);

									MasterUtils.sendStartConditions(servers.get(i), provider, null, null, range, coordinatorRange, replicRange, SHARDING_LEVEL_1);

									//servers.get(i).setAvailable(true);

								}else{
									System.out.println("sending level 2 start conditions (> TWO SERVERS)");

									ServerInfo provider = selectProvider();
									ServerInfo providerReplic = getProviderReplic(provider.getId());
									ServerInfo coordinator = getProviderCoordinator(provider.getId());

									System.out.println(eol + "selected provider is: DB-" + provider.getId());
									System.out.println("    provider replic is: DB-" + providerReplic.getId());
									System.out.println("    provider coordinator is: DB-" + coordinator.getId());

									Range range = MasterUtils.calculateMainRange(provider.getMainRange(), servers.get(i).getId(), numtheaters);
									Range coordinatorRange = provider.getCoordinatorRange();
									Range replicRange = MasterUtils.calculateMainRange(provider.getMainRange(), provider.getId(), numtheaters);

									Range providerMainRange = MasterUtils.calculateReplicRange(provider.getMainRange(), provider.getId(), numtheaters);
									Range providerReplicRange = MasterUtils.calculateReplicRange(provider.getMainRange(), provider.getIdReplic(), numtheaters);

									Range coordinatorReplicRange =  new Range(coordinatorRange.getMin(), coordinatorRange.getMax(), servers.get(i).getId(), numtheaters);

									servers.get(i).setMainRange(range); //Data hold by this server
									servers.get(i).setReplicRange(replicRange); //Data replicated from this server
									servers.get(i).setCoordinatorRange(coordinatorRange); //Data this server replicates

									servers.get(i).setIdReplic(replicRange.getResponsable());// replica id
									servers.get(i).setIdCoordinator(coordinatorRange.getResponsable()); //coordinator id

									provider.setMainRange(providerMainRange);
									provider.setReplicRange(providerReplicRange);
									provider.setCoordinatorRange(range);

									provider.setIdReplic(providerReplicRange.getResponsable());
									provider.setIdCoordinator(range.getResponsable());

									providerReplic.setCoordinatorRange(providerMainRange);

									providerReplic.setIdCoordinator(providerMainRange.getResponsable());

									coordinator.setReplicRange(coordinatorReplicRange);
									coordinator.setIdReplic(coordinatorReplicRange.getResponsable());


									servers.get(i).setAvailable(false);
									servers.get(i).setInMaintenance(true);

									provider.setAvailable(false);
									provider.setInMaintenance(true);

									providerReplic.setAvailable(false);
									providerReplic.setInMaintenance(true);

									coordinator.setAvailable(false);
									coordinator.setInMaintenance(true);


									serversInMaintenance.add(servers.get(i));
									serversInMaintenance.add(provider);
									serversInMaintenance.add(providerReplic);
									if(providerReplic.getId() != coordinator.getId())
										serversInMaintenance.add(coordinator);

									MasterUtils.sendStartConditions(servers.get(i), provider, providerReplic, coordinator, range, coordinatorRange, replicRange, SHARDING_LEVEL_2);

									//servers.get(i).setAvailable(true);

								}
							}
						}
					}
				}
			}

			System.out.println("--------------------------------------------------------");

			break;

		case NodeDeleted:
			System.out.println(eol + "-----------------------EVENT------------------------");
			System.out.println("Received node deletion event");
			System.out.println("Location: " + event.getPath());
			System.out.println();

			//Get the identity of the removed znode
			String nodeName = nodeName(event.getPath());

			if(nodeName.equals("DBready")){
				System.out.println("Leader has departed! Starting leader election...");
				boolean decision = decideNewLeader();
				System.out.println("Leader has been elected!");

				if(decision){
					System.out.println("This server is the new leader!");
					changeToPrimaryMode();
					broadcastLeaderInfo();
					zKeeper.setGroupWatcher("/DBServers", eWatcher);
					setExistentialWatches();
					remotionProcedure(server_id, "DB-" + server_id);

					StoreSystemSnapshot();
				}else{
					System.out.println("New leader is: DB-" + leaderID);
					checkLeaderInfo();
					StoreSystemSnapshot();
				}
			}else{
				int id = Integer.parseInt(nodeName.split("-")[1]);
				remotionProcedure(id, nodeName);
			}

			System.out.println("--------------------------------------------------------");
			break;

		default:
			break;
		}
	}

	private ServerInfo getServer(int id){
		ServerInfo serv = null;

		for(ServerInfo server: servers){
			if(id == server.getId()){
				serv = server;
				break;
			}
		}

		return serv;
	}

	/**
	 * Verifies if this is the new leader.
	 * If not sets a watch for the leader readiness indicator
	 * 
	 * @return	true if is leader. false if not
	 */
	private boolean decideNewLeader(){
		boolean result = false;
		int smallest = server_id;
		List<String> databases = mManager.listDBGroup();

		for(String db: databases){
			if(!db.equals("ID")){
				int dbID = Integer.parseInt(db.split("-")[1]);

				if(dbID < smallest)
					smallest = dbID;
			}
		}

		if(smallest == server_id){
			isLeader = true;
			leaderID = server_id;
			leaderAddress = thisAddress;
			leaderPort = thisPort;
			leaderBindName = dbRegistryName;
			result = true;
		}else{
			leaderAddress = null;
			leaderPort = 0;
			leaderBindName = null;
			leaderID = smallest;
		}

		return result;
	}

	/**
	 * Checks the leader's informations
	 */
	private void checkLeaderInfo(){

		if (mManager.checkDBReady(this)) {
			System.out.println("Reading leader data...");

			//Retrives informations about the leader and sets the
			//watch that alerts the servers to the failure of the primary
			String data = mManager.getDBReadyData(this);
			if (data != null) {
				try {
					String[] dataParts = data.split(",");
					leaderAddress = dataParts[0].split(":")[0];
					leaderPort = Integer.parseInt(dataParts[0]
							.split(":")[1]);
					leaderBindName = dataParts[0].split(":")[2];
					leaderID = Integer.parseInt(dataParts[1]);
				} catch (Exception e) {
					System.out.println("Error reading data");
					e.printStackTrace();
				}
			} else {
				System.out.println("Ready DB data was null");
			}
		}

	}

	private void broadcastLeaderInfo(){

		System.out.print("Broadcasting LeaderInfo...");
		for(ServerInfo serv: servers){
			if(serv.getId() != server_id)
				try {
					//Obtain the server registry
					registry = LocateRegistry.getRegistry(serv.getAddress(),serv.getPort());
					//look up the remote object
					ReceiveMessageInterface db = (ReceiveMessageInterface) (registry.lookup(serv.getBindName()));

					ServerInfo leaderInfo = new ServerInfo(leaderAddress, leaderPort, leaderID);
					leaderInfo.setBindName(leaderBindName);
					db.setLeaderInfo(leaderInfo);

				} catch (AccessException e) {
					e.printStackTrace();
				} catch (RemoteException e) {
					e.printStackTrace();
				} catch (NotBoundException e) {
					e.printStackTrace();
				}
		}
		System.out.println(" Completed!" + eol);
	}

	private void setExistentialWatches(){
		for(ServerInfo serv: servers){
			if(serv.getId() != server_id)
				zKeeper.existsZNode("/DBServers/DB-" + serv.getId(), eWatcher);
		}
	}

	/**
	 * Disables the database tasks of this server 
	 * and sets it in primary mode
	 */
	private void changeToPrimaryMode(){

		try {
			//Obtain this server registry
			registry = LocateRegistry.getRegistry(thisPort);
			// look up the remote object
			ReceiveMessageInterface db = (ReceiveMessageInterface) (registry.lookup(dbRegistryName));

			System.out.println(eol + "Closing database service...");
			db.falha(false, false);

		} catch (AccessException e) {
			e.printStackTrace();
		} catch (RemoteException e) {
			e.printStackTrace();
		} catch (NotBoundException e) {
			e.printStackTrace();
		}

		try {
			System.out.println("Starting primary services");
			registry.rebind(dbRegistryName, this);
		} catch (RemoteException e) {
			System.out.println("ERROR: could not create master registry");
			e.printStackTrace();
		}

		servers.clear();
		getSystemState(true);

		StringBuilder sb = new StringBuilder();
		sb.append(thisAddress);
		sb.append(":");
		sb.append(thisPort);
		sb.append(":");
		sb.append("dbServer" + server_id);
		sb.append(",");
		sb.append(server_id);

		mManager.createDBReady(sb.toString().getBytes());

		/*for(ServerInfo serv: servers){
			System.out.println(serv);
		}*/
	}

	public void remotionProcedure(int id, String nodeName){
		//Determine the removal procedure to be used
		int remotionLevel = servers.size();

		ServerInfo departedServer = null;

		//Removes the server from the servers list
		for(int i=0; i<servers.size(); i++){
			if(servers.get(i).getId() == id){
				departedServer = servers.get(i);
				servers.remove(i);
				break;
			}
		}

		if(remotionLevel == 1){
			System.out.println("Server " + nodeName + " has departed!");
			System.out.println("Initializing level 1 removal procedures");
			System.out.println("No more data servers available");

		}else if(remotionLevel == 2){
			System.out.println("Server " + nodeName + " has departed!");
			System.out.println("Initializing level 2 removal procedures");
			System.out.println("Data servers remaining: " + servers.size());

			int departedServerReplicID = departedServer.getIdReplic();
			ServerInfo departedServerReplic = getServer(departedServerReplicID);

			Range mainRange = MasterUtils.calculateMergeRange(departedServerReplic.getMainRange(), departedServer.getMainRange(), departedServerReplicID);

			departedServerReplic.setMainRange(mainRange);
			departedServerReplic.setCoordinatorRange(null);
			departedServerReplic.setReplicRange(null);

			departedServerReplic.setIdReplic(0);
			departedServerReplic.setIdCoordinator(0);

			departedServerReplic.setAvailable(false);
			departedServerReplic.setInMaintenance(true);

			serversInMaintenance.add(departedServerReplic);

			MasterUtils.sendRemovalConditions(departedServerReplic, null, null, mainRange, null, null, REMOVAL_LEVEL_2);

		}else if(remotionLevel > 2){
			System.out.println("Server " + nodeName + " has departed!");
			System.out.println("Initializing level 3 removal procedures");
			System.out.println("Data servers remaining: " + servers.size());

			int departedServerReplicID = departedServer.getIdReplic();

			ServerInfo departedServerReplic = getServer(departedServerReplicID);
			ServerInfo replic = getServer(departedServerReplic.getIdReplic());
			ServerInfo departedCoordinator = getServer(departedServer.getIdCoordinator());

			Range mainRange = MasterUtils.calculateMergeRange(departedServerReplic.getMainRange(), departedServer.getMainRange(), departedServerReplicID);
			Range coordinatorRange = new Range(departedCoordinator.getMainRange().getMin(), departedCoordinator.getMainRange().getMax(), departedCoordinator.getId(), numtheaters);
			Range replicRange = new Range(mainRange.getMin(), mainRange.getMax(), replic.getId(), numtheaters);

			Range replic_coordinatorRange = MasterUtils.calculateMergeRange(departedServerReplic.getMainRange(), departedServer.getMainRange(), departedServerReplicID);
			Range replic_replicRange = new Range(replic.getReplicRange().getMin(), replic.getReplicRange().getMax(), departedServerReplicID, numtheaters);

			Range departedCoordinator_replicRange = new Range(departedCoordinator.getReplicRange().getMin(), departedCoordinator.getReplicRange().getMax(), departedServerReplicID, numtheaters);

			departedServerReplic.setMainRange(mainRange);
			departedServerReplic.setCoordinatorRange(coordinatorRange);
			departedServerReplic.setReplicRange(replicRange);

			departedServerReplic.setIdCoordinator(coordinatorRange.getResponsable());
			departedServerReplic.setIdReplic(replicRange.getResponsable());

			replic.setCoordinatorRange(replic_coordinatorRange);
			replic.setIdCoordinator(replic_coordinatorRange.getResponsable());

			if(replic.getId() == departedCoordinator.getId()){
				replic.setReplicRange(replic_replicRange);
				replic.setIdReplic(departedServerReplicID);

			}else{
				departedCoordinator.setReplicRange(departedCoordinator_replicRange);
				departedCoordinator.setIdReplic(departedServerReplicID);

			}

			departedServerReplic.setAvailable(false);
			departedServerReplic.setInMaintenance(true);

			replic.setAvailable(false);
			replic.setInMaintenance(true);

			departedCoordinator.setAvailable(false);
			departedCoordinator.setInMaintenance(true);

			serversInMaintenance.add(departedServerReplic);
			serversInMaintenance.add(replic);
			if(replic.getId() != departedCoordinator.getId())
				serversInMaintenance.add(departedCoordinator);

			MasterUtils.sendRemovalConditions(departedServerReplic, departedCoordinator, replic, mainRange, coordinatorRange, replicRange, REMOVAL_LEVEL_3);

		}
	}

	public String getResponsible(String theaterName) throws RemoteException{
		String result = null;

		if(disabled)
			throw new RemoteException();
		
		//System.out.println("Receive Request for " + theaterName);

		for(ServerInfo serv: servers){
			if(serv.isAvailable()){
				if(isInRange(serv.getMainRange().getMin(), serv.getMainRange().getMax(), theaterName)){
					result = serv.getAddress() +":"+ Integer.toString(serv.getPort()) +":"+ serv.getBindName();
					break;
				}
			}
		}

		/*
		if(result != null)
			System.out.println("Redirected to " + result);
		else
			System.out.println("Redirected to null");
		 */
		return result;
	}

	public String listTheaters() throws RemoteException {
		String result = null;

		if(disabled)
			throw new RemoteException();
		
		//System.out.println("Receive Request for list of theaters");

		for(ServerInfo serv: servers){
			if(serv.isAvailable()){
					result = serv.getAddress() +":"+ Integer.toString(serv.getPort()) +":"+ serv.getBindName();
					break;

			}
		}

		/*
		if(result != null)
			System.out.println("Redirected to " + result);
		else
			System.out.println("Redirected to null");
		 */

		return result;
	}


	private boolean isInRange(int min, int max, String theaterName){
		boolean result = false;
		Integer teatroID;

		try {
			teatroID = theatersMap.get(theaterName);

			if(teatroID != null)
				if(max > min){
					if(teatroID >= min && teatroID <= max)
						result = true;
				}
				else{
					if( (teatroID >= min && teatroID <= numtheaters) ||  (teatroID <= max && teatroID >= 1) )
						result = true;
				}


		} catch (NullPointerException e) {
			result = false;
		}

		return result;
	}

	public boolean setReady(int serverID) throws RemoteException {
		boolean result = true;

		if(disabled)
			throw new RemoteException();
		
		for(ServerInfo serv: serversInMaintenance){
			if(serv.getId() == serverID){
				serv.setInMaintenance(false);
				System.out.println(eol + "DB-" + serverID + " finished maintenance");
			}
		}

		if(areAllReadyInMaintenance()){
			System.out.println(eol + "All servers in maintenance are now available");

			for(ServerInfo serv: serversInMaintenance){
				System.out.println("    - DB-" + serv.getId());
				serv.setAvailable(true);
			}
			serversInMaintenance.clear();

			//Stores information about all the existing servers in ZooKeeper
			System.out.println();
			System.out.println("Storing state...");
			if(storeCurrentState())
				System.out.println("Current state stored!");
			else
				System.out.println("Current state failed to be stored!");

			//Stores system snapshot
			System.out.println("Storing snapshot...");
			if(StoreSystemSnapshot())
				System.out.println("Snapshot stored!");
			else
				System.out.println("Snapshot failed to be stored!");

			System.out.println();
		}

		return result;
	}

	private boolean areAllReadyInMaintenance(){
		boolean result = false;
		int nServers = serversInMaintenance.size();
		int count = 0;

		for(ServerInfo serv: serversInMaintenance){
			if(!serv.isInMaintenance())
				count ++;
		}

		if(count == nServers)
			result = true;

		return result;
	}

	private boolean storeCurrentState(){
		boolean result = false;

		String state = SystemStateToString();
		result = mManager.setDBSystemState(state);

		return result;
	}

	private boolean getSystemState(boolean withThisServer){
		boolean result = false;

		String data = mManager.getDBSystemState(null);

		if(data == null)
			result = false;
		else{
			String[] parts = data.split("\\s+");

			for(String str : parts){
				ServerInfo server = new ServerInfo(str);

				if(withThisServer)
					servers.add(server);
				else
					if(server.getId() != server_id)
						servers.add(server);
			}
			result = true;
		}

		return result;
	}

	private String SystemStateToString(){
		StringBuilder sb = new StringBuilder();

		for(ServerInfo server: servers){
			sb.append(server.toString());
			sb.append(' ');
		}

		return sb.toString();
	}

	public boolean falha(boolean failureMode, boolean force) throws RemoteException {
		
		if(disabled)
			throw new RemoteException();
		
		System.out.println("Received: system failure request");
		
		if(failureMode){
			disabled = true;
		}
		if(!failureMode)
			shutDownServerRegistry(failureMode, force);
		return disabled;
	}

	private void shutDownServerRegistry(boolean failure, boolean force) throws RemoteException {
		int succesful = 0;

		if (!failure)
			try {
				registry.unbind(dbRegistryName);
				UnicastRemoteObject.unexportObject(this, force);
			} catch (NotBoundException e) {
				succesful = -1;
			} catch (AccessException e) {
				succesful = -1;
			} catch (UnmarshalException e) {
				System.out.println(e.detail.getMessage());
				succesful = -1;
			} catch (RemoteException e) {
				System.out.println(e.detail.getMessage());
				succesful = -1;
			}
		else {
			try {
				registry.unbind(dbRegistryName);
				registry.rebind(dbRegistryName + "temp", this);
			} catch (RemoteException e) {
				System.out.println("Error: failure mode rebind could not be completed");
				throw e;
			} catch (NotBoundException e) {
				succesful = -1;
			}
		}

		// System.exit(succesful);
	}

	public boolean restart() throws RemoteException {
		System.out.println("Received: system restart request");
		disabled = false;
		//restartServerRegistry();
		return (!disabled);
	}

	private void restartServerRegistry() throws RemoteException {
		try {
			registry.unbind(dbRegistryName + "temp");
			registry.rebind(dbRegistryName, this);
		} catch (RemoteException e) {
			throw e;
		} catch (NotBoundException e) {
		}
	}

	private String nodeName(String path){
		String[] parts = path.split("/");
		String str = parts[parts.length - 1];

		return str;
	}

	private boolean StoreSystemSnapshot(){
		boolean result = true;

		File file = new File(RUNNING_LOCATION + SNAPSHOT_FILE);

		try{
			FileWriter fw = new FileWriter(file);
			BufferedWriter bw = new BufferedWriter(fw);
			bw.write(toString());
			bw.close();
		}catch(IOException e){
			result = false;
			System.out.println("Error: while creating system snapshot");
		}

		return result;
	}

	private String getSystemSnapshot(boolean manager){
		String str = null;
		File file = null;

		if(manager)
			file = new File(RUNNING_LOCATION + SNAPSHOT_MANAGER_FILE);
		else
			file = new File(RUNNING_LOCATION + SNAPSHOT_FILE);

		try{
			FileReader fr = new FileReader(file);
			BufferedReader br = new BufferedReader(fr);

			try {
				StringBuilder sb = new StringBuilder();
				String line = br.readLine();

				while (line != null) {
					sb.append(line);
					sb.append(eol);
					line = br.readLine();
				}
				str = sb.toString();
			} finally {
				br.close();
			}

		}catch(IOException e){
			System.out.println("Error: while creating system snapshot");
		}

		return str;
	}

	private void loadSystemSnapshot(String snapshot){
		String[] parts = snapshot.split(eol)[0].split(masterSeparator);

		server_id = Integer.parseInt(parts[0]);

		if(!parts[1].equals("null"))
			dbRegistryName = parts[1];

		isLeader = Boolean.parseBoolean(parts[2]);
		leaderID = Integer.parseInt(parts[3]);

		if(!parts[4].equals("null"))
			leaderAddress = parts[4];

		leaderPort = Integer.parseInt(parts[5]);

		if(!parts[6].equals("null"))
			leaderBindName = parts[6];

		numtheaters = Integer.parseInt(parts[7]);
		rows = Integer.parseInt(parts[8]);
		columns = Integer.parseInt(parts[9]);

		int nrOfStoredServers = Integer.parseInt(parts[10]);

		for(int i=11; i<=(10 + nrOfStoredServers); i++){
			servers.add(new ServerInfo(parts[i]));
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();

		sb.append(server_id);
		sb.append(masterSeparator);
		sb.append(dbRegistryName);
		sb.append(masterSeparator);
		sb.append(isLeader);
		sb.append(masterSeparator);
		sb.append(leaderID);
		sb.append(masterSeparator);
		sb.append(leaderAddress);
		sb.append(masterSeparator);
		sb.append(leaderPort);
		sb.append(masterSeparator);
		sb.append(leaderBindName);
		sb.append(masterSeparator);
		sb.append(numtheaters);
		sb.append(masterSeparator);
		sb.append(rows);
		sb.append(masterSeparator);
		sb.append(columns);

		sb.append(masterSeparator);
		sb.append(servers.size());

		for(ServerInfo serv : servers){
			sb.append(masterSeparator);
			sb.append(serv.toString());
		}

		return sb.toString();
	}

	public static void main(String args[]) {

		try {
			new Console();
			DBmaster db = new DBmaster();
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}

	}

}
